2.2 Nodes, Topics, Services, and Actions
Learning Objectives
- Understand what ROS 2 nodes are and how they form the computational graph
- Learn how to implement publishers and subscribers for topic-based communication
- Master service-based request-response patterns
- Implement action servers and clients for long-running tasks
- Choose the appropriate communication pattern for different robotics scenarios
Understanding ROS 2 Nodes
A node is an autonomous process that performs a specific computational task within a ROS 2 system. Nodes are the building blocks of robotic applications, each designed to handle a well-defined responsibility.
Node Design Philosophy
The ROS 2 philosophy encourages creating many small, focused nodes rather than monolithic applications. This approach offers several advantages:
Modularity: Each node can be developed, tested, and debugged independently Reusability: Well-designed nodes can be used across different robots and projects Fault Isolation: A crash in one node doesn't necessarily bring down the entire system Language Flexibility: Nodes can be written in different languages (Python, C++) and still communicate Parallel Development: Multiple developers can work on different nodes simultaneously
Example Node Responsibilities
In a humanoid robot system, you might have nodes for:
- IMU sensor driver (publishes orientation data)
- Camera driver (publishes image streams)
- Object detection (processes images, publishes detected objects)
- Trajectory planner (computes walking paths)
- Balance controller (maintains stability)
- Joint state publisher (reports current joint angles)
- Visualizer (displays robot state in RViz)
Each node focuses on one aspect of the robot's functionality, communicating with other nodes through well-defined interfaces.
Topics: Publish-Subscribe Communication
Topics are the primary mechanism for continuous data streaming in ROS 2. They implement a publish-subscribe pattern where data producers (publishers) and data consumers (subscribers) are decoupled.
How Topics Work
- A publisher announces it will publish messages on a specific topic (e.g.,
/camera/image) - Subscribers express interest in receiving messages from that topic
- Through DDS discovery, publishers and subscribers find each other
- When the publisher sends a message, all subscribers receive it
- Communication is asynchronous—publishers don't wait for subscribers
Message Types
Every topic has an associated message type that defines the data structure. ROS 2 provides standard message types:
Common Message Types:
std_msgs/String: Simple text messagesstd_msgs/Int32: Integer valuessensor_msgs/Image: Camera imagessensor_msgs/LaserScan: LiDAR datasensor_msgs/Imu: Inertial measurement unit datageometry_msgs/Twist: Velocity commands (linear and angular)geometry_msgs/Pose: Position and orientation
You can also create custom message types for your specific needs.
Creating a Publisher
Here's a complete example of a publisher node in Python:
"""
Simple publisher that sends greeting messages periodically
"""
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class GreetingPublisher(Node):
"""
A node that publishes greeting messages every second
"""
def __init__(self):
# Initialize the node with a name
super().__init__('greeting_publisher')
# Create a publisher
# - Message type: String
# - Topic name: 'greetings'
# - Queue size: 10 (keeps last 10 messages if subscriber is slow)
self.publisher_ = self.create_publisher(String, 'greetings', 10)
# Create a timer that calls our callback every 1.0 seconds
self.timer = self.create_timer(1.0, self.timer_callback)
# Counter for message numbering
self.count = 0
self.get_logger().info('Greeting Publisher started!')
def timer_callback(self):
"""
Called every second by the timer
Creates and publishes a greeting message
"""
msg = String()
msg.data = f'Hello ROS 2! Message #{self.count}'
# Publish the message
self.publisher_.publish(msg)
# Log what we published
self.get_logger().info(f'Publishing: "{msg.data}"')
self.count += 1
def main(args=None):
"""
Main function to initialize and run the node
"""
# Initialize ROS 2 Python client library
rclpy.init(args=args)
# Create our node
node = GreetingPublisher()
try:
# Keep the node running and processing callbacks
rclpy.spin(node)
except KeyboardInterrupt:
# Handle Ctrl+C gracefully
pass
finally:
# Cleanup
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Creating a Subscriber
Here's the corresponding subscriber node:
"""
Simple subscriber that receives and displays greeting messages
"""
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class GreetingSubscriber(Node):
"""
A node that subscribes to greeting messages and prints them
"""
def __init__(self):
super().__init__('greeting_subscriber')
# Create a subscription
# - Message type: String
# - Topic name: 'greetings' (must match publisher's topic)
# - Callback function: listener_callback
# - Queue size: 10
self.subscription = self.create_subscription(
String,
'greetings',
self.listener_callback,
10
)
self.get_logger().info('Greeting Subscriber started!')
def listener_callback(self, msg):
"""
Called automatically whenever a message arrives
Args:
msg: The received String message
"""
self.get_logger().info(f'Received: "{msg.data}"')
def main(args=None):
rclpy.init(args=args)
node = GreetingSubscriber()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Topic Communication Patterns
One-to-Many: One publisher, multiple subscribers (most common)
- Example: Camera publishes images; object detector, face recognizer, and logger all subscribe
Many-to-One: Multiple publishers, one subscriber
- Example: Multiple sensor nodes publish to
/diagnostics; a monitoring node subscribes
Many-to-Many: Multiple publishers and subscribers
- Example: Multi-robot coordination where all robots publish and subscribe to positions
Services: Request-Response Communication
Services implement a synchronous request-response pattern. A client sends a request and waits for the server to process it and return a response.
When to Use Services
Services are appropriate when:
- You need a response to confirm an operation completed
- The operation is occasional rather than continuous
- The operation is relatively quick (seconds, not minutes)
- You need to pass parameters and get results
Service Example: Add Two Integers
Let's create a service that adds two integers:
Service Server:
"""
Service server that adds two integers
"""
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts
class AdditionServer(Node):
"""
Service server that provides integer addition
"""
def __init__(self):
super().__init__('addition_server')
# Create a service
# - Service type: AddTwoInts (request has a and b, response has sum)
# - Service name: 'add_two_ints'
# - Callback function: add_callback
self.srv = self.create_service(
AddTwoInts,
'add_two_ints',
self.add_callback
)
self.get_logger().info('Addition service ready!')
def add_callback(self, request, response):
"""
Called when a client makes a request
Args:
request: Contains 'a' and 'b' integers
response: Object to fill with result
Returns:
response: Contains 'sum' field
"""
# Perform the addition
response.sum = request.a + request.b
self.get_logger().info(
f'Request: {request.a} + {request.b} = {response.sum}'
)
return response
def main(args=None):
rclpy.init(args=args)
node = AdditionServer()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Service Client:
"""
Service client that requests integer addition
"""
import sys
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts
class AdditionClient(Node):
"""
Service client that calls the addition service
"""
def __init__(self):
super().__init__('addition_client')
# Create a client
self.client = self.create_client(AddTwoInts, 'add_two_ints')
# Wait for the service to be available
while not self.client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('Waiting for service to be available...')
def send_request(self, a, b):
"""
Send an addition request
Args:
a: First integer
b: Second integer
Returns:
Future object that will contain the response
"""
request = AddTwoInts.Request()
request.a = a
request.b = b
self.get_logger().info(f'Sending request: {a} + {b}')
# Call the service asynchronously
future = self.client.call_async(request)
return future
def main(args=None):
rclpy.init(args=args)
# Get numbers from command line arguments
if len(sys.argv) != 3:
print('Usage: addition_client <a> <b>')
return
a = int(sys.argv[1])
b = int(sys.argv[2])
node = AdditionClient()
future = node.send_request(a, b)
# Wait for the result
rclpy.spin_until_future_complete(node, future)
if future.result() is not None:
response = future.result()
node.get_logger().info(f'Result: {response.sum}')
else:
node.get_logger().error('Service call failed!')
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Actions: Long-Running Tasks with Feedback
Actions extend the service pattern for tasks that:
- Take significant time to complete (seconds to minutes)
- Benefit from progress feedback
- May need to be cancelled mid-execution
Action Components
An action has three message types:
- Goal: What you want to accomplish
- Feedback: Periodic progress updates
- Result: Final outcome when complete
Action Example: Fibonacci Sequence
Let's implement an action that calculates Fibonacci numbers with feedback:
Action Server (Simplified):
"""
Action server that computes Fibonacci sequence with feedback
"""
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
from action_tutorials_interfaces.action import Fibonacci
import time
class FibonacciActionServer(Node):
"""
Action server that generates Fibonacci sequences
"""
def __init__(self):
super().__init__('fibonacci_action_server')
# Create an action server
self._action_server = ActionServer(
self,
Fibonacci,
'fibonacci',
self.execute_callback
)
self.get_logger().info('Fibonacci action server ready!')
def execute_callback(self, goal_handle):
"""
Execute the Fibonacci sequence generation
Args:
goal_handle: Contains the goal and allows sending feedback
"""
self.get_logger().info('Executing goal...')
# Get the requested sequence length from goal
order = goal_handle.request.order
# Initialize the sequence
sequence = [0, 1]
# Generate the sequence
for i in range(1, order):
# Check if cancellation was requested
if goal_handle.is_cancel_requested:
goal_handle.canceled()
self.get_logger().info('Goal canceled')
return Fibonacci.Result()
# Calculate next number
sequence.append(sequence[i] + sequence[i-1])
# Send feedback
feedback = Fibonacci.Feedback()
feedback.partial_sequence = sequence
goal_handle.publish_feedback(feedback)
self.get_logger().info(f'Feedback: {sequence}')
# Simulate computation time
time.sleep(1)
# Goal completed successfully
goal_handle.succeed()
# Return the result
result = Fibonacci.Result()
result.sequence = sequence
self.get_logger().info(f'Result: {sequence}')
return result
def main(args=None):
rclpy.init(args=args)
node = FibonacciActionServer()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Real-World Action Examples
Robot Navigation:
- Goal: Target position
- Feedback: Current position, distance remaining
- Result: Success/failure status
Object Manipulation:
- Goal: Object to grasp and target location
- Feedback: Gripper position, grasp status
- Result: Whether object was successfully moved
Autonomous Exploration:
- Goal: Area to explore
- Feedback: Map coverage percentage, current exploring region
- Result: Complete map
Choosing the Right Communication Pattern
| Scenario | Use This | Why |
|---|---|---|
| Continuous sensor data | Topic | Efficient streaming, many subscribers can listen |
| Check if path is collision-free | Service | Quick check with yes/no response |
| Navigate to a position | Action | Long-running with progress updates and cancellation |
| Read robot configuration | Parameter | Static or slowly-changing values |
| Emergency stop | Topic | Needs to reach all nodes quickly |
Key Takeaways
- Nodes are independent processes that perform specific computational tasks in a ROS 2 system
- Topics implement publish-subscribe for continuous data streaming; publishers and subscribers are decoupled
- Publishers send messages without knowing who receives them; subscribers receive all messages on subscribed topics
- Services provide synchronous request-response communication for quick operations needing confirmation
- Actions support long-running tasks with goal submission, periodic feedback, and cancellation capability
- Choose topics for streaming data, services for quick transactions, and actions for time-consuming tasks
- Well-designed nodes are focused, modular, and reusable across different robots and projects
Self-Assessment
Test your understanding of ROS 2 communication patterns:
Question 1
What is the primary purpose of a ROS 2 node?
A) To store robot configuration files
B) To perform a specific computational task
C) To physically connect robot components
D) To compile robot code
Question 2
In the topic communication pattern, what happens if there are no subscribers when a publisher sends a message?
A) The system crashes
B) The message is queued indefinitely
C) The message is sent but not received by anyone
D) The publisher receives an error
Question 3
Which communication pattern should you use for a task that takes 30 seconds and requires progress updates?
A) Topic
B) Service
C) Action
D) Parameter
Question 4
What are the three message types associated with an action?
A) Request, Response, Status
B) Goal, Feedback, Result
C) Start, Progress, End
D) Input, Output, Error
Question 5
In the publisher-subscriber code examples, what does the queue size parameter (10) represent?
A) Maximum number of subscribers
B) Number of messages to keep if subscriber is slower than publisher
C) Publishing frequency in Hz
D) Maximum message size in bytes
Click to reveal answers
Answer Key:
- B - A node is an independent process designed to perform a specific computational task, such as reading a sensor, processing data, or controlling actuators
- C - In publish-subscribe, publishers send messages without waiting for acknowledgment. If no subscribers exist, the message is sent but simply not received by anyone—this is by design for decoupled communication
- C - Actions are specifically designed for long-running tasks that benefit from progress updates and cancellation capability. A 30-second task fits this pattern perfectly
- B - Actions consist of three message types: Goal (what to accomplish), Feedback (progress updates), and Result (final outcome)
- B - The queue size determines how many messages are buffered if the subscriber processes messages slower than the publisher produces them. When the queue is full, oldest messages are discarded
Scoring Guide:
- 5/5: Excellent! You understand ROS 2 communication patterns thoroughly
- 4/5: Great work! Review the concept you missed
- 3/5: Good foundation. Re-read sections on choosing communication patterns
- 2/5 or below: Recommended to review this chapter and practice with the code examples
Next Steps
Now that you understand communication patterns, you're ready to build complete ROS 2 packages. Continue to Building ROS 2 Packages with Python to learn the practical skills of creating, organizing, and running ROS 2 Python packages.